/**
 *    Copyright 2009-2021 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.scripting.xmltags;

import java.util.Map;

import org.apache.ibatis.parsing.GenericTokenParser;
import org.apache.ibatis.session.Configuration;

/**
 * foreach 标签 Sql 节点
 * @author Clinton Begin
 */
public class ForEachSqlNode implements SqlNode {
  public static final String ITEM_PREFIX = "__frch_";

  /** 用于判断循环的终止条件 */
  private final ExpressionEvaluator evaluator;
  /** 迭代的集合表达式 */
  private final String collectionExpression;
  /** 记录了 ForEachSqlNode 节点的子节点 */
  private final SqlNode contents;
  /** 在循环开始前要添加的字符串 */
  private final String open;
  /** 在循环结束后要添加的字符串 */
  private final String close;
  /** 循环过程中，每项之间的分隔符 */
  private final String separator;
  /** 本次迭代的元素。在 Map 中 item是值 */
  private final String item;
  /** 本次迭代的次数。在 Map 中 index是键 */
  private final String index;
  private final Configuration configuration;

  public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
    this.evaluator = new ExpressionEvaluator();
    this.collectionExpression = collectionExpression;
    this.contents = contents;
    this.open = open;
    this.close = close;
    this.separator = separator;
    this.index = index;
    this.item = item;
    this.configuration = configuration;
  }

  /**
   * <pre>
   *     1.解析集合表达式，获取对应的实际参数
   *     2.在循环开始之前，添加 open 字段指定的字符串
   *     3.开始遍历集合，根据遍历的位置和是否指定分割符，用 PrefixedContext 封装 DynamicContext
   *     4.调用 applyIndex() 方法将 index 添加到 DynamicContext.bindings集合中
   *     5.调用 applyItem() 方法将 index 添加到 DynamicContext.bindings集合中
   *     6.转换子节点的“#{}”占为符，此步骤会将 PrefixedContext 封装成 FilteredDynamicContext,在追加子节点转换结果时，
   *        就会使用 FilteredDynamicContext.apply() 方法“#{}”占位符转换成“#{__frch_...}”的格式。返回步骤 3 继续循环
   *     7. 结束循环后，调用 DynamicContext.appendSql()方法添加 close 指定的字符串
   * </pre>
   * @param context 动态的Context
   */
  @Override
  public boolean apply(DynamicContext context) {
    // 1.解析集合表达式，获取对应的实际参数
    Map<String, Object> bindings = context.getBindings();
    // 1.1 解析集合表达式对应的实际参数
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    // 1.2 检测集合长度
    if (!iterable.iterator().hasNext()) {
      return true;
    }
    boolean first = true;
    // 2.在循环开始之前，添加 open 字段指定的字符串
    applyOpen(context);
    int i = 0;
    // 3.开始遍历集合，根据遍历的位置和是否指定分割符，用 PrefixedContext 封装 DynamicContext
    for (Object o : iterable) {
      DynamicContext oldContext = context;
      if (first || separator == null) {
        // 3.1.1 如果是集合的第一项，则将 PrefixedContext.prefix 初始化为空字符串
        context = new PrefixedContext(context, "");
      } else {
        // 3.1.2 如果指定了分隔符，则 PrefixedContext.prefix 初始化为指定分隔符
        context = new PrefixedContext(context, separator);
      }
      // 3.2 uniqueNumber 从 0 开始，每次递增 1，用于转换生成新的“#{}”占位符名称
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709
      // 4.调用applyIndex()方法将 index 添加到 DynamicContext.bindings集合中
      // 5.调用 applyItem() 方法将 index 添加到 DynamicContext.bindings集合中
      if (o instanceof Map.Entry) {
        // 如果集合是 Map 类型，将集合中 key 和 value 添加到 DynamicContext.bindings 集合中保存
        @SuppressWarnings("unchecked")
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
        applyIndex(context, mapEntry.getKey(), uniqueNumber);
        applyItem(context, mapEntry.getValue(), uniqueNumber);
      } else {
        // 将集合中的索引和元素添加到 DynamicContext.bindings 集合中保存
        applyIndex(context, i, uniqueNumber);
        applyItem(context, o, uniqueNumber);
      }
      // 6.转换子节点的“#{}”占为符。调用apply方法进行处理（这里使用的 FilteredDynamicContext 对象）
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      // 6.1 还原成原来的 context
      context = oldContext;
      i++;
    }
    // 7. 结束循环后，调用 DynamicContext.appendSql()方法添加 close 指定的字符串
    applyClose(context);
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
  }

  /**
   * 将 index 添加到 DynamicContext.bindings集合中
   * <pre>
   *     applyIndex() 方法的第三个参数 i,该值由 DynamicContext产生，
   *     且在每个 DynamicContext 对象的生命周期中是单调递增。
   * </pre>
   */
  private void applyIndex(DynamicContext context, Object o, int i) {
    if (index != null) {
      // key 为 index, value 是集合元素
      context.bind(index, o);
      // 为 index 添加前缀和后缀形成新的 key
      context.bind(itemizeItem(index, i), o);
    }
  }

  /**
   * 将 index 添加到 DynamicContext.bindings集合中
   * <pre>
   *    applyItem() 方法的第三个参数 i,该值由 DynamicContext产生，
   *    且在每个 DynamicContext 对象的生命周期中是单调递增。
   * </pre>
   */
  private void applyItem(DynamicContext context, Object o, int i) {
    if (item != null) {
      // key 为 item, value 是集合元素
      context.bind(item, o);
      // 为 item 添加前缀和后缀形成新的 key
      context.bind(itemizeItem(item, i), o);
    }
  }

  /**
   * 添加 open 字段指定的字符串
   */
  private void applyOpen(DynamicContext context) {
    if (open != null) {
      context.appendSql(open);
    }
  }

  /**
   * 添加 close 指定的字符串
   */
  private void applyClose(DynamicContext context) {
    if (close != null) {
      context.appendSql(close);
    }
  }

  /**
   * 添加“__frch_”前缀和 i 后缀
   */
  private static String itemizeItem(String item, int i) {
    return ITEM_PREFIX + item + "_" + i;
  }

  /**
   * 处理“#{}”占位符
   */
  private static class FilteredDynamicContext extends DynamicContext {
    /** 底层封装的 DynamicContext 对象 */
    private final DynamicContext delegate;
    /** 对应集合项在集合中的索引位置 */
    private final int index;
    /** 对应集合项的 index,参见 ForEachSqlNode.index 字段的介绍 */
    private final String itemIndex;
    /** 对应集合项的 item, 参见 ForEachSqlNode.item 字段的介绍 */
    private final String item;

    public FilteredDynamicContext(Configuration configuration,DynamicContext delegate, String itemIndex, String item, int i) {
      super(configuration, null);
      this.delegate = delegate;
      this.index = i;
      this.itemIndex = itemIndex;
      this.item = item;
    }

    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }

    @Override
    public String getSql() {
      return delegate.getSql();
    }

    /**
     * 将 “#{item}"占位符转换成”#{_frch_item_1}"的格式
     * <br/>
     * 将 “#{itemIndex}”占位符转换成”#{_frch_itemIndex_1}"的格式
     * <pre>
     *     "_frch_"是固定的前缀，
     *     "item"与处理前的占位符一样，未发生改变
     *     1则是 FilteredDynamicContext 产生的单调递增值
     * </pre>
     * @param sql
     */
    @Override
    public void appendSql(String sql) {
      // 创建 GenericTokenParser 解析器，注意这里匿名实现的TokenHandler对象
      GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
        // 对 item 进行处理
        String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
        if (itemIndex != null && newContent.equals(content)) {
          // 对 itemIndex 进行处理
          newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
        }
        return "#{" + newContent + "}";
      });

      delegate.appendSql(parser.parse(sql));
    }

    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }

  }


  private class PrefixedContext extends DynamicContext {
    /** 底层封装 DynamicContext 对象 */
    private final DynamicContext delegate;
    /** 指定的前缀 */
    private final String prefix;
    /** 是否已经处理过前缀 */
    private boolean prefixApplied;

    public PrefixedContext(DynamicContext delegate, String prefix) {
      super(configuration, null);
      this.delegate = delegate;
      this.prefix = prefix;
      this.prefixApplied = false;
    }

    public boolean isPrefixApplied() {
      return prefixApplied;
    }

    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }

    @Override
    public void appendSql(String sql) {
      if (!prefixApplied && sql != null && sql.trim().length() > 0) {
        // 追加前缀
        delegate.appendSql(prefix);
        // 表示已经处理过前缀
        prefixApplied = true;
      }
      // 追加 sql 片段
      delegate.appendSql(sql);
    }

    @Override
    public String getSql() {
      return delegate.getSql();
    }

    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }
  }

}
